Skip to content

Stop treating gh API failures as negative answers#53

Merged
Phlogistique merged 13 commits into
mainfrom
claude/gh-failure-conflation
Jul 3, 2026
Merged

Stop treating gh API failures as negative answers#53
Phlogistique merged 13 commits into
mainfrom
claude/gh-failure-conflation

Conversation

@Phlogistique

@Phlogistique Phlogistique commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Two spots swallowed a failed PR listing (gh api .../pulls) and acted on the empty answer (each verified destructive against the pre-fix script with the new test scenarios):

  • has_sibling_conflicts: a failure read as "no siblings", so the resume deleted the kept parent branch while another conflicted PR may still need it for its resolution.
  • The list_child_prs callers in main(): the process substitutions consuming it drop its exit status, so a failure read as "no children" and the run deleted the merged branch under the children it never saw, without updating them.

has_sibling_conflicts now dies on the failed listing; the list_child_prs callers capture its output and die on failure.

Stacked on #52.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH


Generated by Claude Code

claude added 4 commits June 9, 2026 21:37
…esolution

If origin/<old base> was deleted while a child PR sat in conflict
(auto-delete head branches left enabled despite the README, or manual
deletion), the resume merge failed with "not something we can merge" and
git merge --abort failed too ("There is no merge to abort"). The run then
exited nonzero after re-posting the misleading conflict comment and the
label, repeating on every push. (Not a mid-function set -e kill:
update_direct_target is called in an if condition, which suppresses
errexit; observed via the new test scenario.)

Check origin/<old base> up front like #39 did for the target branch, and
only run git merge --abort when MERGE_HEAD exists.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
set -e is suppressed inside if/&&/|| conditions and everything they call,
including the whole body of update_direct_target, which both entry points
invoke as a condition. So most failures there fell through silently: a
failed git checkout let the merges and the final git reset --hard run on
the previously checked-out branch (corrupting it, in main's loop over
several child PRs), and a failed git commit-tree let the function return
success, after which main retargeted the PR onto a base its head does not
contain. A failed conflict comment still added the label, leaving a resume
with no state marker.

Every log_cmd call site now states its failure policy: run (log, execute,
die on failure; die's explicit exit aborts from any context) or try (log,
execute, hand the status to the caller) for the merges whose failure is an
expected outcome. set -e stays as a backstop only.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
Two helpers swallowed gh errors with `|| echo ""` and returned the negative
answer instead (verified against the previous script version with the new
test scenarios):

- has_sibling_conflicts: a failed `gh pr list` read as "no siblings", so the
  resume deleted the kept parent branch while another conflicted PR may
  still need it for its resolution.
- pr_has_conflict_label: a failed `gh pr view` read as "no label", ending
  the run green without resuming anything.

Both now die on command failure, so the branch and label survive and the
next push retries. read_state_marker gets the same explicit handling; there
the gh failure already aborted the run (pipefail propagated it to an
errexit-active caller), but silently, with the error message hidden by
2>/dev/null.

Also reorders main() to push the updated heads before retargeting their
PRs, matching the conflict-resolved path: if the run fails between the two
steps, a retargeted PR with a stale head sits conflicting with its new
base, GitHub suppresses its CI, and no label exists to resume it.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
Re-applied onto the restructured script: the merge --abort guard and the
origin/OLD_BASE existence check (now via the PR-number abandon_resume).
The regression test becomes scenario F, adapted to the PR_BASE payload.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
Phlogistique added a commit that referenced this pull request Jun 11, 2026
#55)

If `origin/<old base>` was deleted while a child PR sat in conflict
(auto-delete head branches left enabled despite the README, or manual
deletion), the resume merge failed with "not something we can merge" and
`git merge --abort` failed too ("There is no merge to abort"). The run
then exited nonzero after re-posting the misleading conflict comment and
the label, repeating on every push. (Not a mid-function `set -e` kill:
`update_direct_target` is called in an `if` condition, which suppresses
errexit; observed via the new test scenario.)

Check `origin/<old base>` up front like the existing target-branch
check, and only run `git merge --abort` when `MERGE_HEAD` exists.

Base of the stack continued in #52 and #53.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH

---
_Generated by [Claude
Code](https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH)_

Co-authored-by: Claude <noreply@anthropic.com>
claude and others added 9 commits June 11, 2026 13:12
…on main

Re-applied the run/try/die conversion over the restructured script: the
helpers from the PR-number and merge-method work are converted too, and the
explicit read-failure aborts in read_state_marker / pr_has_conflict_label
become die (regression scenarios G and H). list_child_prs gets try, not run:
its callers consume it through a process substitution, which swallows the
status either way.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
Trunk meanwhile absorbed most of this PR (#42 reordered main(), #50
hardened the label and comment reads), so what remains is the
has_sibling_conflicts fix plus a new one of the same kind: list_child_prs
failures were swallowed by the process substitutions consuming it, so a
failed listing read as 'no children' and let main() delete the merged
branch under the children it never saw. Callers now capture the output and
die on failure.

https://claude.ai/code/session_01STkeSJ7cLrmrNn4aTDYkwH
…appers

# Conflicts:
#	tests/test_conflict_resolution_resume.sh
#	update-pr-stack.sh
…flation

# Conflicts:
#	tests/mock_gh.sh
#	tests/test_conflict_resolution_resume.sh
#	update-pr-stack.sh
@github-actions github-actions Bot changed the base branch from claude/run-try-die-wrappers to main July 3, 2026 11:11
@Phlogistique Phlogistique marked this pull request as ready for review July 3, 2026 11:11
@Phlogistique Phlogistique merged commit 066613a into main Jul 3, 2026
@github-actions github-actions Bot deleted the claude/gh-failure-conflation branch July 3, 2026 12:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants